package de.mizi.ui.messagebox;

import java.awt.Dimension;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;

import javax.swing.JPanel;

import de.mizi.controller.GameEvent;
import de.mizi.controller.GameEventType;
import de.mizi.controller.GameListener;
import de.mizi.utility.FontData;

/**
 * A message box that displays lines of text.
 * @author mizi
 *
 */
public class MessageBox extends JPanel implements GameListener {
	private FontData fontData;
	private ArrayList<String> originalMessages;
	private ArrayList<String> messagesToDraw;
	private int visibleLines;
	private int linesToMemorize;

	private static final long serialVersionUID = 1L;
	
	/**
	 * Constructs a new MessageBox object with the given FontData object. The
	 * number of visible lines determines how many lines of text will actually
	 * be drawn to the screen. The number of lines to memorize will determine
	 * how many lines of text will be memorized before a line will be removed,
	 * starting with the oldest line.
	 * @param fontData the FontData object for this MessageBox object
	 * @param visibleLines the number of visible lines
	 * @param linesToMemorize the number of lines to memorize
	 */
	public MessageBox(FontData fontData, int visibleLines, int linesToMemorize) {
		this.fontData = fontData;
		this.originalMessages = new ArrayList<String>();
		this.messagesToDraw = new ArrayList<String>();
		this.visibleLines = visibleLines < linesToMemorize ? visibleLines : linesToMemorize;
		this.linesToMemorize = linesToMemorize;
	}
	
	@Override
	public void setPreferredSize(Dimension preferredSize) {
		super.setPreferredSize(preferredSize);
		updateMessagesToDraw();
	}
	
	@Override
	public void setSize(Dimension d) {
		super.setSize(d);
		updateMessagesToDraw();
	}
	
	@Override
	public void setSize(int width, int height) {
		super.setSize(width, height);
		updateMessagesToDraw();
	}
	
	/**
	 * Adds a new message to this MessageBox object. Use the newline character
	 * '\n' to add multiple messages.
	 * @param message the new message
	 */
	public void addMessage(String message) {
		String[] splitMessages = message.split("\n");
		// Iterate the array in reverse order, because each message will be added at the beginning.
		for(int splitIndex = splitMessages.length - 1; splitIndex >= 0; --splitIndex) {
			// Insert the new message at the beginning.
			originalMessages.add(0, splitMessages[splitIndex]);
		}
		removeOldOriginalMessages();
		updateMessagesToDraw();
		repaint();
	}
	
	/**
	 * First clears all the lines of this MessageBox object and then 
	 * adds the given message to this MessageBox object. Use the newline
	 * character '\n' to add multiple messages.
	 * @param message the new message
	 */
	public void setMessage(String message) {
		clearMessages();
		addMessage(message);
		repaint();
	}
	
	/**
	 * Get the number of messages that are currently memorized by this
	 * MessageBox object.
	 * @return the number of currently memorized messages
	 */
	public int getNumberOfMessages() {
		return originalMessages.size();
	}
	
	/**
	 * Get the current maximum height of this MessageBox object, depending on
	 * the font size and the current number of visible lines.
	 * @return the maximum height in pixel
	 */
	public int getMaximumHeight() {
		return visibleLines * fontData.height;
	}

	/**
	 * Get the number of visible lines for this MessageBox object.
	 * @return the number of visible lines
	 */
	public int getVisibleLines() {
		return visibleLines;
	}

	/**
	 * Set the number of visible lines for this MessageBox object.
	 * @param visibleLines the number of visible lines
	 */
	public void setVisibleLines(int visibleLines) {
		if(visibleLines <= linesToMemorize) {
			this.visibleLines = visibleLines;
		}
		repaint();
	}

	/**
	 * Get the number of lines to memorize for this MessageBox object.
	 * @return the number of lines to memorize
	 */
	public int getLinesToMemorize() {
		return linesToMemorize;
	}

	/**
	 * Set the number of lines to memorize for this MessageBox object.
	 * @param linesToMemorize the number of lines to memorize
	 */
	public void setLinesToMemorize(int linesToMemorize) {
		this.linesToMemorize = linesToMemorize;
		removeOldOriginalMessages();
		removeOldMessagesToDraw();
		repaint();
	}
	
	/**
	 * Removes all messages of this MessageBox object.
	 */
	public void clearMessages() {
		originalMessages.clear();
		messagesToDraw.clear();
		repaint();
	}
	
	@Override
	public void update(GameEvent event) {
		if(event.eventType == GameEventType.NEW_MESSAGE) {
			addMessage(event.getNewMessage());
			repaint();
		}
	}
	
	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.setColor(fontData.fontColor);
		g.setFont(fontData.font);
		for(int currentLine = 0; currentLine < messagesToDraw.size() && currentLine < visibleLines; ++currentLine) {
			g.drawString(messagesToDraw.get(currentLine), 0, currentLine * fontData.height - fontData.yOffset);
		}
	}
	
	private void updateMessagesToDraw() {
		messagesToDraw.clear();
		// Iterate the list in reverse order, because each message will be added at the beginning.
		for(int originalIndex = originalMessages.size() - 1; originalIndex >= 0; --originalIndex) {
			// Add the split message at the beginning.
			messagesToDraw.addAll(0, fitMessageToWidth(originalMessages.get(originalIndex)));
		}
		removeOldMessagesToDraw();
	}
	
	// TODO: Try to speed this algorithm up. Its very slow when the messages have no space characters or if there is not enough space for space characters.
	private Collection<String> fitMessageToWidth(String message) {
		LinkedList<String> fittingMessages = new LinkedList<String>();
		StringBuilder messageBuffer = new StringBuilder(message.trim());
		StringBuilder notFittingMessage;
		int cutOffIndex = -1;
		
		while(!messageBuffer.toString().isEmpty()) {
			notFittingMessage = new StringBuilder();
			while(!messageFitsTheWidth(messageBuffer.toString())) {
				// Try to find a spacecharacter.
				cutOffIndex = messageBuffer.lastIndexOf(" ", messageBuffer.length() - 1);
				if(cutOffIndex < 0) {
					// No spacecharacter was found, so cut off the last character of the message.
					cutOffIndex = messageBuffer.length() - 1;
				}
				// Memorize the string that will be cut off. Then cut off the message.
				// (Do not append to the memorised string, everything will be reversed if you do.)
				notFittingMessage.insert(0, messageBuffer.substring(cutOffIndex));
				messageBuffer.delete(cutOffIndex, messageBuffer.length());
			}
			fittingMessages.add(messageBuffer.toString());
			// Remove the leading and trailing spaces and put the not fitting part of the message into the messagebuffer.
			messageBuffer = new StringBuilder(notFittingMessage.toString().trim());
		}
		
		return fittingMessages;
	}
	
	private boolean messageFitsTheWidth(String message) {
		int messageWidth = (int)fontData.getStringBounds(message).getWidth();
		// Each message with a length of just one (or less) character will fit the width.
		// This is to prevent an endless loop in the fitMessageToWidth-method.
		return message.length() <= 1 || messageWidth <= getWidth();
	}
	
	private void removeOldOriginalMessages() {
		// Remove the old messages at the end of the list of messages.
		while(originalMessages.size() > linesToMemorize) {
			originalMessages.remove(originalMessages.size() - 1);
		}
	}
	
	private void removeOldMessagesToDraw() {
		// Remove the old messages at the end of the list of messages.
		while(messagesToDraw.size() > linesToMemorize) {
			messagesToDraw.remove(messagesToDraw.size() - 1);
		}
	}
}
